<?php

/* $Id: Plexus.php 13 2007-03-13 19:41:36Z dreamscape $ */
/**
 * Plexus \Plex"us\, n. An intricate or interwoven combination of elements or parts 
 * in a coherent structure.
 * @package Plexus
 */

/**
 * short description.
 *
 * long description.
 *
 * @version $Rev: 13 $
 * @author Josh Dechant <josh@flexiecommerce.com>
 * @copyright Copyright 2006 JD Productions, LLC. All Rights Reserved.
 * @package Plexus
 */
abstract class Plugin {

    /**
     * @var boolean Allow this Plugin to overload "hook" calls?
     */
    public $overloadHooks = false;

    /**
     * @var Plexus The current {@link Plexus} that has loaded this Plugin.
     */
    public $plexus = null;

    /**
     * @var object Object The current object being plugged into.
     */
    protected $baseObject = null;


    const REPLACED_METHOD = 'Indicates that a method with void return has been replaced';

    /**
     * Load the current object being plugged into.
     * @param object $object The current object being plugged into.
     */
    public function loadBaseObject($object) {
        $this->baseObject = $object;
    }

    /**
     * Load the current {@link Plexus} that has loaded this Plugin.
     * @param Plexus $object The current {@link Plexus} that has loaded this Plugin.
     */
    public function loadPlexus(Plexus $plexus) {
        $this->plexus = $plexus;
    }

    function __call($method, $args) {
        if ((strpos($method, 'do') === 0) && ($this->plexus !== null)) {
            array_unshift($args, $this);
            $return = call_user_func_array(array($this->plexus, $method), $args);
            return $return;
        }
    }

}

/**
 * short description.
 * 
 * long description.
 *
 * @version $Rev: 13 $
 * @author Josh Dechant <josh@flexiecommerce.com>
 * @copyright Copyright 2006 JD Productions, LLC. All Rights Reserved.
 * @package Plexus
 */
class PluginCollection {

    protected $plugins = array();

    /**
     * @throws Exception when encounters plugin not of class Plugin.
     */
    public function __construct(array $plugins) {
        foreach ($plugins as $plugin) {
            // another plugin collection passed in
            if (($plugin instanceof PluginCollection) === true) {
                $this->plugins = array_merge($this->plugins, $plugin->plugins);
                continue;
            }

            if (is_object($plugin) === false) {
                $plugin = new $plugin();
            }

            if (($plugin instanceof Plugin) === false) {
                $class = get_class($plugin);
                if (!($parent = get_parent_class($plugin))) {
                    $parent = $class;
                }
                throw new Exception(
                        'Expected "' . $class . '" to be of class Plugin; was "' . $parent . '" instead.'
                );
            }

            $this->plugins[get_class($plugin)] = $plugin;
        }
    }

    protected function dispatchMethod($method, $args) {
        $return = null;

        $baseObject = array_shift($args);

        $canExecute = true;

        foreach ($this->plugins as $plugin) {
            if (!method_exists($plugin, $method) && ($plugin->overloadHooks === false)) {
                continue;
            }

            // If a plugin is dispatching don't change the baseObject.
            // If another plugin needs to access the dispatching plugin,
            //   it can use $this->plexus->PluginName.
            if (($baseObject instanceof Plugin) === false) {
                $plugin->loadBaseObject($baseObject);
                $plugin->loadPlexus($baseObject->plexus);
            }

            $tmp_return = call_user_func_array(array($plugin, $method), $args);

            if (strpos($method, 'around') === 0 && $tmp_return === false) {
                $canExecute = false;
            }

            if ($tmp_return !== null) {
                $return = $tmp_return;
            }
        }

        if (strpos($method, 'around') === 0) {
            $return = $canExecute;
        }

        return $return;
    }

    function __get($property) {
        if (isset($this->plugins[$property])) {
            return $this->plugins[$property];
        }
        return false;
    }

    function __call($method, $args) {
        $return = $this->dispatchMethod($method, $args);
        if (strpos($method, 'around') === 0) {
            if ($return === null) {
                $return = true;
            } elseif ($return === Plugin::REPLACED_METHOD) {
                $return = null;
            }
        }
        return $return;
    }

}

/**
 * Plexus \Plex"us\, n. An intricate or interwoven combination of elements or parts 
 * in a coherent structure.
 * 
 * long description.
 *
 * @version $Rev: 13 $
 * @author Josh Dechant <josh@flexiecommerce.com>
 * @copyright Copyright 2006 JD Productions, LLC. All Rights Reserved.
 * @package Plexus
 */
class Plexus {

    /**
     * @var PluginCollection The {@link PluginCollection} for this Plexus.
     */
    protected $plugins;

    public function __construct(PluginCollection $plugins) {
        $this->plugins = $plugins;
    }

    /**
     * Check if a {@link Plugin} from the {@link PluginCollection} for this Plexus exists.
     * {@source}
     * Use like <code>isset($plexus->PluginName)</code>
     * @param string $property Name of the {@link Plugin} to check.
     * @return boolean
     */
    function __isset($property) {
        if ($this->plugins->$property !== false) {
            return true;
        }
        return false;
    }

    /**
     * Get a {@link Plugin} from the {@link PluginCollection} for this Plexus.
     * {@source}
     * Use like <code>$plexus->PluginName</code>
     * @param string $property Name of the {@link Plugin} to get.
     * @return Plugin
     */
    function __get($property) {
        if ($this->plugins->$property !== false) {
            return $this->plugins->$property;
        }
        throw new Exception(
                'Plugin of name "' . $property . '" not found in Plexus ' . $this
        );
    }

    function __call($method, array $args) {
        if (substr($method, 0, 2) == 'do') {
            return $this->__plexusCall(substr($method, 2), $args);
        }
    }

    protected function __plexusCall($method, array $args) {
        if ((sizeof($args) > 0) && is_object($args[0])) {
            $baseObject = $args[0];
            $baseObject->plexus = $this;
        } else {
            $baseObject = $this;
            $baseObject->plexus = $this;
            array_unshift($args, $baseObject);
        }

        $method = strtolower($method[0]) . substr($method, 1);

        $className = get_parent_class($baseObject);
        if ($className === false) {
            $className = get_class($baseObject);
        }

        $plugin_method = ucfirst($className) . ucfirst($method);

        // before hook - runs before method; allows parameters to be changed
        $before_args = $args;
        array_shift($before_args);
        $args[] = & $before_args;
        call_user_func_array(array($this->plugins, 'before' . $plugin_method), $args);
        $args = $before_args;
        array_unshift($args, $baseObject);

        $can_run = true;
        $return = false;
        if (method_exists($baseObject, $method) || method_exists($baseObject, '__call')) {
            $can_run = call_user_func_array(array($this->plugins, 'around' . $plugin_method), $args);

            // method is allowed to run
            if ($can_run === true) {
                $object_args = $args;
                array_shift($object_args);
                $return = call_user_func_array(array($baseObject, $method), $object_args);



                // have replaced the method
            } elseif ($can_run !== false) {
                $return = $can_run;
            }
        }

        if ($can_run !== false) {
            $on_args = $args;
            $on_args[] = & $return;
            call_user_func_array(array($this->plugins, 'on' . $plugin_method), $on_args);
        }

        call_user_func_array(array($this->plugins, 'after' . $plugin_method), $args);

        return $return;
    }

}
